Claude Chrome Testing Workflow Copy

Documentation for Developers

Claude + Chrome: Visual Testing Workflow

The Problem

Claude runs inside a lightweight Linux VM. Your Chrome browser runs on your Mac. There is no network bridge between them. This means:

This is not a bug — it's a security boundary by design. The VM is sandboxed.

The Solution: Chrome JavaScript Injection

Claude has access to your Chrome browser via the "Claude in Chrome" extension. This extension provides tools to:

The key insight: we don't need to serve files. We inject code directly into an existing page.

How It Works (Step by Step)

Step 1: Open a "Host" Page

Navigate to a simple, publicly reachable website. We use https://example.com/ because:

Step 2: Replace the Page with document.write()

Using the Chrome JavaScript tool, we execute:

document.write("<html>...our entire HTML structure...</html>");

This completely replaces example.com's DOM with our custom page. The CSS, canvas element, layout — everything gets injected in this single call.

Important: The HTML must be properly escaped for JavaScript string context. Single quotes in CSS (like font-family: 'Segoe UI') will break if the outer string uses single quotes. Use JSON-style double-quote escaping or template strings.

Step 3: Inject Large Scripts via Base64 Chunking

The Chrome JS tool has a practical size limit per call (~4-8KB of text). Our VittoEngine bundle is ~21KB minified. To handle this:

Working Approach: Text-Based Chunking

  1. Encode the entire engine JS as base64 (produces ~28KB of base64 text)
  2. Split the base64 TEXT string into ~7000-char chunks (not binary chunks — this is critical)
  3. Inject each chunk sequentially, accumulating on a window variable:
// First chunk
window._B = "SGVsbG8gV29ybGQ...first 7000 chars of base64...";

// Subsequent chunks
window._B += "...next 7000 chars of base64...";
window._B += "...next 7000 chars of base64...";
// ... repeat until all chunks loaded (typically 4 chunks total)
  1. Decode and inject as a script element:
var code = atob(window._B);
var s = document.createElement('script');
s.setAttribute('data-vitto', 'v2');
s.textContent = code;
document.head.appendChild(s);

CRITICAL: Do NOT split the binary file first then base64 each chunk separately. That creates padded chunks that corrupt when concatenated. Base64 the entire bundle first, then split the resulting base64 text.

This produces window.VittoEngine global with all exports (VittoRenderer, MOOD_DEFINITIONS, etc.).

Step 4: Why Script Element Injection?

Compared to new Function(atob(...)):

Step 5: Inject the UI Script

The UI harness code (mood buttons, sliders, inspector) is smaller (~9KB) and can be injected in 2-3 Chrome JS tool calls directly as executable JavaScript. No base64 needed — just split at natural boundaries (e.g., after mood grid setup, after slider creation, after inspector).

All references to the renderer and state must be stored on window.* (e.g., window._renderer, window._sliderEls) since each Chrome JS tool call runs in a separate evaluation context.

The harness includes:

Step 6: Take Screenshots and Interact

Once everything is injected:

Practical Gotchas

Chrome Throttles Background Tabs

When a tab isn't in the foreground, Chrome throttles requestAnimationFrame to ~1fps. This means:

var target = VittoEngine.MOOD_DEFINITIONS['sleepy'];
var s = window._renderer.getState();
s.tT = 1.0; // Skip transition
for (var k of VittoEngine.MOOD_NUMERIC_KEYS) {
  if (target[k] !== undefined) s[k] = target[k];
}
s.bg = [...target.bg];

Size Limits per JS Call

Keep each Chrome JS tool call under ~4000 characters of JavaScript. For larger payloads:

The Tab Persists Until Navigated Away

Once injected, the page stays alive as long as the tab isn't refreshed or navigated. You CAN interact with it from your side too (click mood buttons, drag sliders, move mouse over the canvas).

However: If you refresh the tab, everything disappears (it goes back to example.com). The injection is purely in-memory.

What YOU See vs What Claude Sees

You (Mac) Claude (VM)
localhost:8765 Connection refused Would work inside VM only
example.com tab Vitto harness (after injection) Can screenshot + interact
Dev harness HTML file Opens locally in your browser Can't serve to your browser

Setup Latency & Performance

The Chrome injection process requires 4+ separate JS tool calls just for base64 chunks. This causes setup latency:

This is a known pain point for rapid iteration. Accept the overhead for visual verification, but for tight design-test loops, prefer local testing with the HTML file.

Preparing the Engine for Injection

Before a Chrome testing session, the engine must be built:

# From the monorepo root (your machine or Claude's VM)
cd /path/to/gspace1
npx esbuild libs/libs-anim/vitto-avatar/src/lib/engine/index.ts \
  --bundle --format=iife --global-name=VittoEngine \
  --outfile=dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js \
  --minify --sourcemap

The output should be ~21KB minified IIFE.

Then to prepare base64 chunks:

# With Python for accurate ~7000-char chunks:
python3 -c "
import base64
with open('dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js', 'rb') as f:
    encoded = base64.b64encode(f.read()).decode()
chunk_size = 7000
for i in range(0, len(encoded), chunk_size):
    chunk = encoded[i:i+chunk_size]
    with open(f'/tmp/b64chunk_{i // chunk_size}.txt', 'w') as out:
        out.write(chunk)
    print(f'Chunk {i // chunk_size}: {len(chunk)} chars')
print(f'Total base64 length: {len(encoded)}')
"

This produces typically 4 chunks of ~7000 chars each from the ~28KB base64 string.

For Future Sessions: Quick Reference

When Claude needs to visually test the Vitto avatar in Chrome:

  1. tabs_context_mcp — find or create a tab
  2. Navigate to example.com (or reuse an existing injection tab)
  3. Inject HTML structure via document.write()
  4. Build engine if not built: npx esbuild ...
  5. Base64 encode the entire bundle first
  6. Split base64 TEXT string into ~7000-char chunks (typically 4 chunks)
  7. Inject chunks sequentially onto window._B with += concatenation
  8. Decode and inject as script element: atob(window._B) → create script → append to head
  9. Inject UI harness script in 2-3 parts
  10. Take screenshots, change moods, adjust sliders, iterate

Idle Behaviors

The engine includes noise-driven idle animations that make Vitto feel alive even without mood changes:

These are controlled by the enableIdleBehaviors config flag and can be toggled via the "Idle Behaviors" checkbox in the UI harness.

Your Local Workflow (Paco)

You don't need any of this! You can:

  1. Open vitto animation/vitto-dev-harness.html directly in your browser
  2. It loads the engine from ../gspace1/dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js
  3. Full 60fps, mouse tracking, all controls work natively
  4. Prerequisite: Run the esbuild command first so the bundle exists at that path

Or once we wire up the Angular route:

  1. yarn serve:gspace1
  2. Navigate to localhost:6200/dev/vitto-avatar (or whatever route we create)
  3. Full Angular integration with live reload

Why This Matters

This workflow lets both of us iterate on the same codebase:

Debugging Tips

If the injection fails:

  1. Check bundle size: ls -lh dist/libs/libs-anim/vitto-avatar/engine/vitto-engine.iife.js
  2. Verify base64 length matches Python output
  3. Check that each chunk injection completes before the next
  4. If decoder fails, verify window._B contains all concatenated chunks
  5. Check Chrome console for decode errors (via read_console_messages)

If rendering is slow:

  1. Remember background tabs are throttled to ~1fps — this is normal
  2. Use force-complete transition method to speed up visual feedback
  3. For rapid iteration, test locally in the HTML file at 60fps first